/*
 * Copyright 2004-2022 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
 * versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the Licence for the specific language governing permissions and limitations
 * under the Licence.
*//*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.http.localserver;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;

import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpResponseFactory;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpServerConnection;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.DefaultHttpResponseFactory;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.SyncBasicHttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpExpectationVerifier;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestHandler;
import org.apache.http.protocol.HttpRequestHandlerRegistry;
import org.apache.http.protocol.HttpService;
import org.apache.http.protocol.ImmutableHttpProcessor;
import org.apache.http.protocol.ResponseConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseDate;
import org.apache.http.protocol.ResponseServer;

/**
 * Local HTTP server for tests that require one. Based on the
 * <code>ElementalHttpServer</code> example in HttpCore.
 */
public class LocalTestServer {
    /**
     * The local address to bind to. The host is an IP number rather than
     * "localhost" to avoid surprises on hosts that map "localhost" to an IPv6
     * address or something else. The port is 0 to let the system pick one.
     */
    public final static InetSocketAddress TEST_SERVER_ADDR = new InetSocketAddress("127.0.0.1", 0);

    /**
     * The request handler registry.
     */
    private final HttpRequestHandlerRegistry handlerRegistry;

    private final HttpService httpservice;

    /**
     * Optional SSL context
     */
    private final SSLContext sslcontext;

    /**
     * The server socket, while being served.
     */
    private volatile ServerSocket servicedSocket;

    /**
     * The request listening thread, while listening.
     */
    private volatile ListenerThread listenerThread;

    /**
     * Set of active worker threads
     */
    private final Set<Worker> workers;

    /**
     * The number of connections this accepted.
     */
    private final AtomicInteger acceptedConnections = new AtomicInteger(0);

    /**
     * Creates a new test server.
     *
     * @param proc       the HTTP processors to be used by the server, or
     *                   <code>null</code> to use a {@link #newProcessor default}
     *                   processor
     * @param reuseStrat the connection reuse strategy to be used by the server, or
     *                   <code>null</code> to use {@link #newConnectionReuseStrategy()
     *                   default} strategy.
     * @param params     the parameters to be used by the server, or <code>null</code> to
     *                   use {@link #newDefaultParams default} parameters
     * @param sslcontext optional SSL context if the server is to leverage SSL/TLS
     *                   transport security
     */
    public LocalTestServer(final BasicHttpProcessor proc, final ConnectionReuseStrategy reuseStrat,
                           final HttpResponseFactory responseFactory, final HttpExpectationVerifier expectationVerifier,
                           final HttpParams params, final SSLContext sslcontext) {
        super();
        this.handlerRegistry = new HttpRequestHandlerRegistry();
        this.workers = Collections.synchronizedSet(new HashSet<Worker>());
        this.httpservice = new HttpService(proc != null ? proc : newProcessor(), reuseStrat != null ? reuseStrat
                : newConnectionReuseStrategy(), responseFactory != null ? responseFactory : newHttpResponseFactory(),
                handlerRegistry, expectationVerifier, params != null ? params : newDefaultParams());
        this.sslcontext = sslcontext;
    }

    /**
     * Creates a new test server with SSL/TLS encryption.
     *
     * @param sslcontext SSL context
     */
    public LocalTestServer(final SSLContext sslcontext) {
        this(null, null, null, null, null, sslcontext);
    }

    /**
     * Creates a new test server.
     *
     * @param proc   the HTTP processors to be used by the server, or
     *               <code>null</code> to use a {@link #newProcessor default}
     *               processor
     * @param params the parameters to be used by the server, or <code>null</code> to
     *               use {@link #newDefaultParams default} parameters
     */
    public LocalTestServer(BasicHttpProcessor proc, HttpParams params) {
        this(proc, null, null, null, params, null);
    }

    /**
     * Obtains an HTTP protocol processor with default interceptors.
     *
     * @return a protocol processor for server-side use
     */
    protected HttpProcessor newProcessor() {
        return new ImmutableHttpProcessor(new HttpResponseInterceptor[]{new ResponseDate(), new ResponseServer(),
                new ResponseContent(), new ResponseConnControl()});
    }

    /**
     * Obtains a set of reasonable default parameters for a server.
     *
     * @return default parameters
     */
    protected HttpParams newDefaultParams() {
        HttpParams params = new SyncBasicHttpParams();
        params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, 60000)
                .setIntParameter(CoreConnectionPNames.SOCKET_BUFFER_SIZE, 8 * 1024)
                .setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false)
                .setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, true)
                .setParameter(CoreProtocolPNames.ORIGIN_SERVER, "LocalTestServer/1.1");
        return params;
    }

    protected ConnectionReuseStrategy newConnectionReuseStrategy() {
        return new DefaultConnectionReuseStrategy();
    }

    protected HttpResponseFactory newHttpResponseFactory() {
        return new DefaultHttpResponseFactory();
    }

    /**
     * Returns the number of connections this test server has accepted.
     */
    public int getAcceptedConnectionCount() {
        return acceptedConnections.get();
    }

    /**
     * {@link #register Registers} a set of default request handlers.
     * <p/>
     * <pre>
     * URI pattern      Handler
     * -----------      -------
     * /echo/*          {@link EchoHandler EchoHandler}
     * /random/*        {@link RandomHandler RandomHandler}
     * </pre>
     */
    public void registerDefaultHandlers() {
        handlerRegistry.register("/echo/*", new EchoHandler());
        handlerRegistry.register("/random/*", new RandomHandler());
    }

    /**
     * Registers a handler with the local registry.
     *
     * @param pattern the URL pattern to match
     * @param handler the handler to apply
     */
    public void register(String pattern, HttpRequestHandler handler) {
        handlerRegistry.register(pattern, handler);
    }

    /**
     * Unregisters a handler from the local registry.
     *
     * @param pattern the URL pattern
     */
    public void unregister(String pattern) {
        handlerRegistry.unregister(pattern);
    }

    /**
     * Starts this test server.
     */
    public void start() throws Exception {
        if (servicedSocket != null) {
            throw new IllegalStateException(this.toString() + " already running");
        }
        ServerSocket ssock;
        if (sslcontext != null) {
            SSLServerSocketFactory sf = sslcontext.getServerSocketFactory();
            ssock = sf.createServerSocket();
        } else {
            ssock = new ServerSocket();
        }

        ssock.setReuseAddress(true); // probably pointless for port '0'
        ssock.bind(TEST_SERVER_ADDR);
        servicedSocket = ssock;

        listenerThread = new ListenerThread();
        listenerThread.setDaemon(false);
        listenerThread.start();
    }

    /**
     * Stops this test server.
     */
    public void stop() throws Exception {
        if (servicedSocket == null) {
            return; // not running
        }
        ListenerThread t = listenerThread;
        if (t != null) {
            t.shutdown();
        }
        synchronized (workers) {
            for (Iterator<Worker> it = workers.iterator(); it.hasNext(); ) {
                Worker worker = it.next();
                worker.shutdown();
            }
        }
    }

    public void awaitTermination(long timeMs) throws InterruptedException {
        if (listenerThread != null) {
            listenerThread.join(timeMs);
        }
    }

    @Override
    public String toString() {
        ServerSocket ssock = servicedSocket; // avoid synchronization
        StringBuilder sb = new StringBuilder(80);
        sb.append("LocalTestServer/");
        if (ssock == null) {
            sb.append("stopped");
        } else {
            sb.append(ssock.getLocalSocketAddress());
        }
        return sb.toString();
    }

    /**
     * Obtains the local address the server is listening on
     *
     * @return the service address
     */
    public InetSocketAddress getServiceAddress() {
        ServerSocket ssock = servicedSocket; // avoid synchronization
        if (ssock == null) {
            throw new IllegalStateException("not running");
        }
        return (InetSocketAddress) ssock.getLocalSocketAddress();
    }

    /**
     * The request listener. Accepts incoming connections and launches a service
     * thread.
     */
    class ListenerThread extends Thread {

        private volatile Exception exception;

        ListenerThread() {
            super();
        }

        @Override
        public void run() {
            try {
                while (!interrupted()) {
                    Socket socket = servicedSocket.accept();
                    acceptedConnections.incrementAndGet();
                    DefaultHttpServerConnection conn = new DefaultHttpServerConnection();
                    conn.bind(socket, httpservice.getParams());
                    // Start worker thread
                    Worker worker = new Worker(conn);
                    workers.add(worker);
                    worker.setDaemon(true);
                    worker.start();
                }
            } catch (Exception ex) {
                this.exception = ex;
            } finally {
                try {
                    servicedSocket.close();
                } catch (IOException ignore) {
                }
            }
        }

        public void shutdown() {
            interrupt();
            try {
                servicedSocket.close();
            } catch (IOException ignore) {
            }
        }

        public Exception getException() {
            return this.exception;
        }

    }

    class Worker extends Thread {

        private final HttpServerConnection conn;

        private volatile Exception exception;

        public Worker(final HttpServerConnection conn) {
            this.conn = conn;
        }

        @Override
        public void run() {
            HttpContext context = new BasicHttpContext();
            try {
                while (this.conn.isOpen() && !Thread.interrupted()) {
                    httpservice.handleRequest(this.conn, context);
                }
            } catch (Exception ex) {
                this.exception = ex;
            } finally {
                workers.remove(this);
                try {
                    this.conn.shutdown();
                } catch (IOException ignore) {
                }
            }
        }

        public void shutdown() {
            interrupt();
            try {
                this.conn.shutdown();
            } catch (IOException ignore) {
            }
        }

        public Exception getException() {
            return this.exception;
        }

    }
}
